Skip to content

fix(draft): credit a Swiss bye as a match win (odd pods) - #6351

Merged
matthewevans merged 4 commits into
phase-rs:mainfrom
boskodev790:fix/swiss-bye-match-win
Aug 1, 2026
Merged

fix(draft): credit a Swiss bye as a match win (odd pods)#6351
matthewevans merged 4 commits into
phase-rs:mainfrom
boskodev790:fix/swiss-bye-match-win

Conversation

@boskodev790

@boskodev790 boskodev790 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #6349: in an odd-sized Swiss pod, generate_swiss_pairings (crates/draft-core/src/session.rs) leaves one player unpaired each round — they take a bye — but the bye was never credited. match_wins stayed 0, so Swiss standings (sorted by match_wins) undercounted the bye player. DraftSession::new accepts any seats.len() (Swiss has no even/8 guard, unlike SingleElimination), so odd pods are reachable.

generate_swiss_pairings now returns the round's pairings plus the bye player (Option<PlayerId>), and apply_generate_pairings credits the bye a match win via the same ensure_match_record(..).match_wins += 1 path a normal win uses. SingleElimination is unaffected.

Files changed

  • crates/draft-core/src/session.rs — return the bye from generate_swiss_pairings; credit it in apply_generate_pairings; regression test.

Implementation method

Method: not-applicable — this is draft-core tournament pairing/standings, not crates/engine/ game logic, so /engine-implementer does not apply. Targeted fix (return-type widened to report the bye) + one regression test; no new types or public API beyond the internal fn's return shape.

CR references

None — Swiss tournament structure is not part of the MTG Comprehensive Rules (CR governs game rules; pairing/standings are tournament policy).

Track

Developer — Rust nightly-2026-04-19 (the repo's pinned toolchain) is installed locally. I built and ran the test on the real workspace build (not just CI):

  • cargo test -p draft-core swiss_bye_in_odd_pod1 passed.
  • Test-first check: with the credit line disabled, the same test fails left: 0, right: 1 — so the test genuinely catches the bug.
  • Gate A run locally (below).
  • clippy deferred to CI: repeated local cargo clippy runs were cut short by environment build-lock/timeout contention; the change mirrors existing idioms in the same file, so no new lint surface is expected.

LLM

Model: claude-opus-4-8
Thinking: high

Verification

  • Gate A output below is for the current committed head.
  • Anchors cite existing analogous code at the same seam.
  • cargo test -p draft-core swiss_bye_in_odd_podok, 1 passed (and fails 0 != 1 with the credit disabled — test-first verified).
  • scripts/check-parser-combinators.shGate A PASS (no parser files touched).
  • clippy / full cargo test — deferred to CI (local build-lock contention).

Gate A

Gate A PASS head=4378726a9aa302acf1c3f80c63c2e3ad2268210b base=21fc5f3b476021ea78e38b712cbffe0ae417a5d9

Anchored on

  • crates/draft-core/src/session.rs:311apply_match_record_result credits a match win with ensure_match_record(records, winner_pid).match_wins += 1; the bye credit reuses that exact house pattern.
  • crates/draft-core/src/session.rs:118apply_generate_pairings already special-cases tournament pod size (SingleElimination's non-8 guard); crediting the odd-Swiss bye is the same class of pod-size edge handling in the same function.

Summary by CodeRabbit

  • New Features
    • Swiss tournament pairing now identifies the player receiving a bye in odd-sized pods.
    • Bye recipients receive credit for a match win.
  • Bug Fixes
    • Prevented generating Swiss pairings for a round that is not the next required round.
    • Prevented duplicate bye credit when attempting to generate the same round again.
  • Tests
    • Added coverage for bye assignment, player records, and duplicate round generation.

An odd-pod Swiss round leaves one player unpaired (a bye), but the bye was
never credited — match_wins stayed 0, so Swiss standings (sorted by
match_wins) undercounted the bye player. generate_swiss_pairings now reports
the bye alongside the pairings, and apply_generate_pairings credits it a
match win via the same ensure_match_record(..).match_wins += 1 path used for
a normal win. SingleElimination is unaffected (it already rejects non-8 pods).

Added a regression test: a 3-seat Swiss round-1 credits exactly one match win
(the bye); it fails on the pre-fix code (0 != 1) and passes after.
@superagent-security superagent-security Bot added the contributor:flagged Contributor flagged for review by trust analysis. label Jul 22, 2026
@superagent-security

Copy link
Copy Markdown

🚨 Contributor flagged. Click here for more info: Superagent Dashboard

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: aabc1155-eb35-4e42-b5fa-c0399f0a6b78

📥 Commits

Reviewing files that changed from the base of the PR and between 4378726 and 3619c6e.

📒 Files selected for processing (1)
  • crates/draft-core/src/session.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/draft-core/src/session.rs

📝 Walkthrough

Walkthrough

Swiss pairing generation now validates sequential rounds, returns the unpaired player as an optional bye, and credits that player with a match win. Tests verify the bye recipient and reject duplicate round generation without duplicate credit.

Changes

Swiss bye match-win credit

Layer / File(s) Summary
Validate pairing round generation
crates/draft-core/src/session.rs
Pairing generation now rejects rounds other than the next sequential round and uses a shared tuple-based result path for Swiss and single-elimination pairings.
Return and credit the Swiss bye
crates/draft-core/src/session.rs
generate_swiss_pairings returns the unpaired player as an optional bye. Odd-pod tests verify the bye player's match win and reject duplicate credit when the round is generated again.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the Swiss bye match-win fix for odd-sized pods.
Linked Issues check ✅ Passed The changes credit the Swiss bye as a match win and add regression coverage for a three-seat session [#6349].
Out of Scope Changes check ✅ Passed The changes remain within Swiss bye handling, duplicate-round protection, and related regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/draft-core/src/session.rs (1)

1749-1752: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the specific bye player’s win.

The aggregate total passes even if a paired player is credited. Derive the unpaired PlayerId from the generated pairing and assert that record has exactly one match_win, while paired players remain unchanged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/draft-core/src/session.rs` around lines 1749 - 1752, Replace the
aggregate total_match_wins assertion in the bye-handling test with an assertion
targeting the specific unpaired PlayerId from the generated pairing. Derive that
bye player, verify its match record has exactly one match_win, and retain checks
that paired players’ records remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/draft-core/src/session.rs`:
- Around line 131-143: Make the GeneratePairings flow around
generate_swiss_pairings and the new_pairings insertion idempotent for repeated
calls on the same Swiss round. Detect whether pairings for round already exist
before appending or crediting swiss_bye, and ensure an existing round cannot
duplicate pairings or increment match_wins again while preserving normal
generation for new rounds.

---

Nitpick comments:
In `@crates/draft-core/src/session.rs`:
- Around line 1749-1752: Replace the aggregate total_match_wins assertion in the
bye-handling test with an assertion targeting the specific unpaired PlayerId
from the generated pairing. Derive that bye player, verify its match record has
exactly one match_win, and retain checks that paired players’ records remain
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e09f8399-2e0d-4689-9c83-f69ed57fd3f1

📥 Commits

Reviewing files that changed from the base of the PR and between 7195a8e and 4378726.

📒 Files selected for processing (1)
  • crates/draft-core/src/session.rs

Comment on lines +131 to +143
let (new_pairings, swiss_bye) = match session.config.tournament_format {
TournamentFormat::Swiss => generate_swiss_pairings(session, round, &mut rng),
TournamentFormat::SingleElimination => generate_se_pairings(session, round),
TournamentFormat::SingleElimination => (generate_se_pairings(session, round), None),
};

for p in &new_pairings {
session.pairings.push(p.clone());
}
// A Swiss bye counts as a match win for the unpaired player; without this credit an
// odd-pod bye scores nothing and Swiss standings (sorted by match_wins) are wrong.
if let Some(bye) = swiss_bye {
ensure_match_record(&mut session.match_records, bye).match_wins += 1;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make Swiss bye credit idempotent.

GeneratePairings is permitted after RoundComplete, but this path does not verify that round is new or that pairings for it are absent. Repeating generation for an odd Swiss round can append another pairing set and increment bye standings again, corrupting match totals. Validate round uniqueness/monotonicity or make pairing insertion and bye credit idempotent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/draft-core/src/session.rs` around lines 131 - 143, Make the
GeneratePairings flow around generate_swiss_pairings and the new_pairings
insertion idempotent for repeated calls on the same Swiss round. Detect whether
pairings for round already exist before appending or crediting swiss_bye, and
ensure an existing round cannot duplicate pairings or increment match_wins again
while preserving normal generation for new rounds.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocked — replaying an already generated Swiss round duplicates pairings and the bye win.

🔴 Blocker

crates/draft-core/src/session.rs:91-143 permits GeneratePairings from RoundComplete and appends generated pairings plus swiss_bye without checking whether round already exists. Since round is supplied by the action, replaying an existing odd Swiss round appends a second set of pairings and increments the bye player’s match_wins again, corrupting standings. The CodeRabbit finding is confirmed against the current head.

crates/draft-core/src/session.rs:1749-1752 only asserts the aggregate win total, so it also passes if the credit goes to a paired player. Add a regression that attempts the same round again after RoundComplete, verifies no duplicate pairing/bye credit (or a clear transition error), and asserts the specific unpaired player has exactly one match win while the paired players remain unchanged.

Recommendation: request changes for round uniqueness/idempotence and a discriminating specific-bye test.

@matthewevans matthewevans self-assigned this Jul 29, 2026
@matthewevans

Copy link
Copy Markdown
Member

This pull request still has unresolved requested changes, and there has been no contributor follow-up on the current head.

Please push a commit that addresses the existing requested changes and request re-review within 7 days of this warning. If the requested changes remain unaddressed after that window, this pull request will be automatically closed.

@matthewevans matthewevans added the bug Bug fix label Aug 1, 2026
@matthewevans

Copy link
Copy Markdown
Member

Maintainer follow-up: the prior current-head review found the Swiss bye/replay fix correct, but CI failed only cargo fmt --check. I applied the formatter without semantic changes in 3619c6ec4da8921ee4c6a21472652b6d3c497ae3 (three line wraps in the new regression test). The original replay guard and specific-bye assertions remain intact. Fresh CI is required on this new head before approval or queueing.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current-head re-review approved. The sequential-round guard rejects replay before any pairing or bye mutation, and the regression identifies the specific bye player while confirming paired players remain unchanged. All current checks are green.

@matthewevans
matthewevans added this pull request to the merge queue Aug 1, 2026
@matthewevans matthewevans removed their assignment Aug 1, 2026
Merged via the queue into phase-rs:main with commit 945a927 Aug 1, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix contributor:flagged Contributor flagged for review by trust analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants